﻿using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using LitJson;

public abstract class PUEXEmitter : MonoBehaviour {
	
	protected static int __spawnIndex = 0;

	#region - Editable property
	[Header("Emitter:")]
	public string emitterName = "";
	public string positionParamName = "";
	public string rotationParamName = "";
	public string scaleParamName = "";
	
	[Header("--- setting:")]
	public float duration;
	public bool looping;
	public int loopCount;
	public bool prewarm;
	public float startDelay;
	public bool keepLocal;
	public bool orientationFollowDirection;
	public int maxParticles;

	[Header("--- emission:")]
	public PUEXDynamicAttribute emissionRate;
	public PUEXDynamicTrigger emissionBursts;
	public int burstMap;
	public float emissionTimeStep;
	
	[Header("--- particle start live time:")]
	public PUEXDynamicAttribute startTotalTimeToLive;
	[Header("--- particle start speed:")]
	public PUEXDynamicAttribute startSpeed;
	[Header("--- particle start rotation:")]
	public PUEXDynamicAttribute startRotation;
	public PUEXDynamicAttribute startRotationSpeed;
	public Vector3 rotationAxis;
	[Header("--- particle start size:")]
	public PUEXDynamicAttribute startSize;
	public PUEXDynamicAttribute startScaleX;
	public PUEXDynamicAttribute startScaleY;
	[Header("--- particle start color:")]
	public PUEXDynamicColor startColor;
	[Header("--- particle start uv:")]
	public Vector2 uvTiles;
	public bool startRandomFrame;
	[Header("--- particle start mass:")]
	public PUEXDynamicAttribute startMass;
	#endregion

	#region - Property
	[HideInInspector]
	public bool autoStart;
	[HideInInspector]
	protected bool _enabled;
	public bool isEnabled () { 
		return _enabled; 
	}
	public virtual void enable () {
		if (_enabled == false) {
			foreach (var affector in affectors)
				affector.start ();
			
			foreach (var handle in handles)
				handle.start ();
			
			if (render)
				render.start ();

			_remainder = 0f;
			_timeSinceLastEmission = 0f;

			_durationRemain = duration;
			_startDelayRemain = startDelay;
			_loopCount = 0;
			timeFraction = 0f;

			_enabled = true;
			_running = true;
			if (prewarm)
				_needPrewarm = true;

			notifyEmitterStart();
		}
	}
	public virtual void disable () {
		if (_enabled == true) {
			foreach (var particle in particlePool) {
				if (particle.alive) {
					particle.destroy();
				}
			}
			particlePool.Clear ();
			aliveParticleCount = 0;

			_enabled = false;
			_running = false;

			notifyEmitterEnd();

			foreach (var affector in affectors)
				affector.stop ();
			
			foreach (var handle in handles)
				handle.stop ();
			
			if (render)
				render.stop ();
		}
	}
	
	protected bool _prepared;
	protected bool _running;
	protected bool _needPrewarm;
	public bool isRunning () { return _running; }

	protected float _remainder;
	protected float _timeSinceLastEmission;

	protected float _durationRemain;
	protected float _startDelayRemain;
	protected int _loopCount;
	[HideInInspector]
	public float timeFraction;

	public Vector3 position {
		get {
			return transform.localPosition;
		}
		set {
			transform.localPosition = value;
		}
	}
	public Quaternion rotation {
		get {
			return transform.localRotation;
		}
		set {
			transform.localRotation = value;
		}
	}
	public Vector3 scale {
		get {
			return transform.localScale;
		}
		set {
			transform.localScale = value;
		}
	}
	
	[HideInInspector]
	protected bool _visible;
	public void setVisible (bool visible) {
		_visible = visible;
	}
	public bool isVisible () {
		return _visible;
	}

	[HideInInspector]
	public Matrix4x4 emitterToSimulationTransform;
	[HideInInspector]
	public Vector3 emitterToSimulationScale;
	
	[HideInInspector]
	public PUEXParticleSystem ownerParticleSystem;
	[HideInInspector]
	public List<PUEXParticle> particlePool;
	[HideInInspector]
	public int aliveParticleCount;
	[HideInInspector]
	public PUEXAffector[] affectors;
	[HideInInspector]
	public PUEXHandle[] handles;
	[HideInInspector]
	public PUEXRender render;
	#endregion

	#region - Lifecycle
	public PUEXEmitter ()
	{
		duration = 10f;
		looping = false;
		prewarm = false;
		startDelay = 0;
		keepLocal = true;
		orientationFollowDirection = true;
		maxParticles = 300;

		startTotalTimeToLive = PUEXDynamicAttribute.GenerateFixed (5f);
		startSpeed = PUEXDynamicAttribute.GenerateFixed (1f);
		startRotation = PUEXDynamicAttribute.GenerateFixed (0f);
		startRotationSpeed = PUEXDynamicAttribute.GenerateFixed (0f);
		startSize = PUEXDynamicAttribute.GenerateFixed (1f);
		startScaleX = PUEXDynamicAttribute.GenerateFixed (1f);
		startScaleY = PUEXDynamicAttribute.GenerateFixed (1f);
		startColor = new PUEXDynamicColor ();
		uvTiles = new Vector2 (1, 1);
		startRandomFrame = false;
		startMass = PUEXDynamicAttribute.GenerateFixed (1f);
		rotationAxis = Vector3.forward;

		emissionRate = PUEXDynamicAttribute.GenerateFixed (10f);
		emissionBursts = new PUEXDynamicTrigger ();
		burstMap = 50;
		emissionTimeStep = 0f;

		autoStart = true;
		_enabled = false;
		_prepared = false;
		_running = false;
		_needPrewarm = false;
		_remainder = 0f;
		_timeSinceLastEmission = 0f;
		_durationRemain = 0f;
		_startDelayRemain = 0f;
		_loopCount = 0;
		_visible = true;
		ownerParticleSystem = null;
		aliveParticleCount = 0;
	}

	public void Reset ()
	{
		// need parent particle system
		if (transform.parent) {
			var system = transform.parent.GetComponent<PUEXParticleSystem> ();
			if (system == null) {
				Debug.LogError ("<PUEX> no particle system with emitter");
			}
		} else {
			Debug.LogError ("<PUEX> no particle system with emitter");
		}

		// only one emitter on one game object
		var emitters = gameObject.GetComponents<PUEXEmitter> ();
		if (emitters.Length > 1)
		{
			Debug.LogError("<PUEX> more than 1 emitter on gameobject");
		}

		// auto name
		if (emitterName.Length == 0) {
			string typeName = GetType().Name;
			typeName = typeName.Replace("PUEX", "");
			emitterName = gameObject.name + " " + typeName + " " + ++__spawnIndex;
		}
	}

	public void Awake ()
	{
		affectors = GetComponents<PUEXAffector> ();
		handles = GetComponents<PUEXHandle> ();
		render = GetComponent<PUEXRender> ();
		
		particlePool = new List<PUEXParticle> (maxParticles);
		aliveParticleCount = 0;
	}

	public void OnEnable ()
	{
		if (Application.isPlaying && _prepared) {
			enable ();
		} else {
			autoStart = true;
		}
	}
	
	public void OnDisable ()
	{
		if (Application.isPlaying && _prepared) {
			disable ();
		} else {
			autoStart = false;
		}
	}
	#endregion

	#region - State
	public virtual void prepared (PUEXParticleSystem system)
	{
		if (!_prepared) {
			this.ownerParticleSystem = system;

			foreach (var affector in affectors)
				affector.prepared (this);

			foreach (var handle in handles)
				handle.prepared (this);

			if (render)
				render.prepared (this);

			_prepared = true;
		}
	}
	
	public virtual void start ()
	{
		if (autoStart)
			enable ();
	}
	
	public virtual void pause ()
	{
		foreach (var affector in affectors)
			affector.pause ();

		foreach (var handle in handles)
			handle.pause ();

		if (render)
			render.pause ();
	}
	
	public virtual void resume ()
	{
		foreach (var affector in affectors)
			affector.resume ();

		foreach (var handle in handles)
			handle.resume ();

		if (render)
			render.resume ();
	}
	
	public virtual void stop ()
	{
		_durationRemain = 0f;
		_running = false;
	}
	
	public virtual void unPrepared ()
	{
		disable ();

		if (_prepared) {
			foreach (var affector in affectors)
				affector.unPrepared ();

			foreach (var handle in handles)
				handle.unPrepared ();

			if (render)
				render.unPrepared ();

			_prepared = false;
		}
	}
	#endregion
	
	#region - Update
	public virtual void update (float dt)
	{
		if (!_enabled)
			return;

		if (_startDelayRemain > 0.0f) {
			_startDelayRemain -= dt;
			return;
		}

		if (_needPrewarm) {
			doPrewarm ();
			return;
		}

		updateTransform ();

		// preupdate
		foreach (var affector in affectors) {
			if (affector.isEnabled())
				affector.preUpdate(dt);
		}

		int requested = calculateRequestedParticles (dt);
		emitParticles (requested);

		// update
		foreach (var particle in particlePool) {
			if (particle.alive) {
				foreach (var affector in affectors) {
					if (affector.isEnabled())
						affector.update(dt, particle);
				}
			}
		}

		foreach (var particle in particlePool) {
			if (particle.alive) {
				particle.process(dt, orientationFollowDirection);

				// process sub particle system
				if (particle.attachedSystem && (particle.attachedSystem.inheritOwnerTranslation || particle.attachedSystem.inheritOwnerRotation || particle.attachedSystem.inheritOwnerScale)) {
					if (keepLocal) {
						if (particle.attachedSystem.inheritOwnerTranslation)
							particle.attachedSystem.position = particle.position;
						if (particle.attachedSystem.inheritOwnerRotation)
							particle.attachedSystem.rotation = particle.orientation;
						if (particle.attachedSystem.inheritOwnerScale)
							particle.attachedSystem.scale = particle.dimensions();
					} else {
						Matrix4x4 systemInvMat = ownerParticleSystem.systemToWorldTransform.inverse;
						Matrix4x4 mat = systemInvMat * particle.transform();

						Vector3 s = Vector3.one;
						Quaternion r = Quaternion.identity;
						Vector3 p = Vector3.zero;
						PUEXUtils.matrixDecompose(mat, ref s, ref r, ref p);
						
						if (particle.attachedSystem.inheritOwnerTranslation)
							particle.attachedSystem.position = p;
						if (particle.attachedSystem.inheritOwnerRotation)
							particle.attachedSystem.rotation = r;
						if (particle.attachedSystem.inheritOwnerScale)
							particle.attachedSystem.scale = s;
					}
				}
			}
		}

		// postupdate
		foreach (var affector in affectors) {
			if (affector.isEnabled())
				affector.postUpdate(dt);
		}

		foreach (var particle in particlePool) {
			if (particle.alive) {
				if (particle.hasEventFlags(PUEXParticle.PEF_COLLIDED)) {
					notifyParticleCollided(particle);
				}
				if (particle.hasEventFlags(PUEXParticle.PEF_EXPIRED)) {
					notifyParticleExpiration(particle);
					particle.destroy();
					removeParticle(particle);
				}
				particle.eventFlags = PUEXParticle.PEF_NONE;
			}
		}

		_durationRemain -= dt;
		timeFraction = (duration - _durationRemain) / duration;
		if (_durationRemain <= 0f) {
			if (loopCount > 0 && _loopCount < loopCount)
				_loopCount++;

			if (looping && (loopCount <= 0 || _loopCount < loopCount)) {
				_durationRemain = duration + _durationRemain;
				timeFraction = (duration - _durationRemain) / duration;
			} else {
				if (aliveParticleCount <= 0) {
					disable ();
				} else {
					_durationRemain = 0f;
					timeFraction = 1f;
					_running = false;
				}
			}
		}
	}

	protected void doPrewarm () {
		_needPrewarm = false;
		int n = (int)Mathf.Ceil (duration / Time.fixedDeltaTime);
		for (int i = 0; i < n; i++) {
			update (Time.fixedDeltaTime);
		}
	}

	protected void updateTransform () {
		if (keepLocal) {
			emitterToSimulationTransform = Matrix4x4.TRS (position, rotation, scale);
			emitterToSimulationScale = scale;
		} else {
			emitterToSimulationTransform = ownerParticleSystem.systemToWorldTransform * Matrix4x4.TRS(position, rotation, scale);
			emitterToSimulationScale = Vector3.Scale(ownerParticleSystem.systemToWorldScale3, scale);
		}
	}
	
	public virtual void visit ()
	{
		if (_enabled && _visible && render) {
			render.visit ();
		}
	}
	
	public virtual void draw ()
	{
		if (_enabled && _visible && render) {
			render.draw();
		}
	}
	#endregion

	#region - Emit
	protected virtual int calculateRequestedParticles (float dt)
	{
		if (!_running)
			return 0;

		float rate = emissionRate.getValue (timeFraction);
		_remainder += rate * dt;

		float burst = 0;
		if (emissionBursts.isTrigger(timeFraction, out burst)) {
			_remainder += (int)(burst * burstMap);
		}
		
		int requestedParticles = 0;
		if (emissionTimeStep > 0f) {
			_timeSinceLastEmission += dt;
			if (_timeSinceLastEmission >= emissionTimeStep) {
				_timeSinceLastEmission -= emissionTimeStep;
				
				requestedParticles = (int)_remainder;
				_remainder -= requestedParticles;
			}
		} else {
			requestedParticles = (int)_remainder;
			_remainder -= requestedParticles;
		}

		return requestedParticles;
	}
	
	public virtual void emitParticles(int requested)
	{
		if (!_enabled)
			return;

		if (aliveParticleCount + requested > maxParticles)
			requested = maxParticles - aliveParticleCount;

		for (int i = 0; i < requested; i++)
		{
			PUEXParticle particle = emitNewParticle();
			if (particle == null) 
				return;
			
			particle.initForEmission();

			initParticleTimeToLive(particle, i, requested);
			initParticlePosition(particle, i, requested);
			initParticleVelocity(particle, i, requested);
			initParticleRotation(particle, i, requested);
			initParticleSize(particle, i, requested);
			initParticleColor(particle, i, requested);
			initParticleMass(particle, i, requested);
			initParticleTextureCoords(particle, i, requested);

			notifyParticleEmission(particle);
		}
	}

	protected virtual void initParticleTimeToLive (PUEXParticle particle, int index, int requested)
	{
		particle.timeToLive = particle.totalTimeToLive = startTotalTimeToLive.getValue (timeFraction);
	}
	
	protected virtual void initParticlePosition (PUEXParticle particle, int index, int requested)
	{
		particle.position = particle.originalPosition = emitterToSimulationTransform.MultiplyPoint (Vector3.zero);
	}
	
	protected virtual void initParticleVelocity (PUEXParticle particle, int index, int requested)
	{
		particle.direction = particle.originalDirection = emitterToSimulationTransform.MultiplyVector (Vector3.forward).normalized;
		particle.speed = particle.originalSpeed = startSpeed.getValue (timeFraction);
	}
	
	protected virtual void initParticleRotation (PUEXParticle particle, int index, int requested)
	{
		// orientation align
		particle.orientation = particle.originalOrientation = Quaternion.LookRotation(particle.direction, Vector3.up);

		particle.rotation = particle.originalRotation = startRotation.getValue (timeFraction);

		particle.rotationAxis = particle.originalRotationAxis = rotationAxis;
		particle.rotationSpeed = particle.originalRotationSpeed = startRotationSpeed.getValue (timeFraction);
	}

	protected virtual void initParticleSize (PUEXParticle particle, int index, int requested)
	{
		particle.size = particle.originalSize = startSize.getValue (timeFraction);
		particle.scale = particle.originalScale = new Vector2 (startScaleX.getValue(timeFraction), startScaleY.getValue (timeFraction));
	}

	protected virtual void initParticleColor (PUEXParticle particle, int index, int requested)
	{
		particle.color = particle.originalColor = startColor.getColor (timeFraction);
	}
	
	protected virtual void initParticleMass (PUEXParticle particle, int index, int requested)
	{
		particle.mass = particle.originalMass = startMass.getValue (timeFraction);
	}
	
	protected virtual void initParticleTextureCoords (PUEXParticle particle, int index, int requested)
	{
		int ut = (int)uvTiles.x > 0 ? (int)uvTiles.x : 1;
		int vt = (int)uvTiles.y > 0 ? (int)uvTiles.y : 1;

		if (startRandomFrame) {
			int frame = (int)(Random.value * (ut * vt));
			particle.uvOffset = particle.originalUVOffset = new Vector2 (frame % ut, frame / ut);
		} else {
			particle.uvOffset = particle.originalUVOffset = Vector2.zero;
		}

		particle.uvScale = new Vector2 (1f / ut, 1f / vt);
	}
	
	protected PUEXParticle emitNewParticle ()
	{
		aliveParticleCount++;

		foreach (var particle in particlePool) {
			if (!particle.alive)
				return particle;
		}

		var newParticle = new PUEXParticle ();
		particlePool.Add (newParticle);
		return newParticle;
	}
	
	protected void removeParticle (PUEXParticle particle)
	{
		aliveParticleCount--;
	}
	#endregion

	#region - Event
	protected virtual void notifyEmitterStart ()
	{
		foreach (var handle in handles) {
			handle.notifyEmitterStart();
		}
	}
	
	protected virtual void notifyEmitterEnd ()
	{
		foreach (var handle in handles) {
			handle.notifyEmitterEnd();
		}
	}

	protected virtual void notifyParticleEmission (PUEXParticle particle)
	{
		foreach (var handle in handles) {
			handle.notifyParticleEmission(particle);
		}
	}

	protected virtual void notifyParticleExpiration (PUEXParticle particle)
	{
		foreach (var handle in handles) {
			handle.notifyParticleExpiration(particle);
		}
	}

	protected virtual void notifyParticleCollided (PUEXParticle particle)
	{
		foreach (var handle in handles) {
			handle.notifyParticleCollided(particle);
		}
	}

	public virtual void notifyEvent (string eventName)
	{
		if (!_enabled)
			return;

		foreach (var handle in handles) {
			handle.notifyEvent(eventName);
		}
	}
	#endregion

	#region - Param property
	public virtual void updateParams (PUEXParticleSystem system)
	{
		Vector3 value;
		if (system.getParamVec3 (positionParamName, out value)) {
			position = value;
		}
		if (system.getParamVec3 (rotationParamName, out value)) {
			rotation = Quaternion.Euler(value);
		}
		if (system.getParamVec3 (scaleParamName, out value)) {
			scale = value;
		}

		emissionRate.parameterize(system);
		emissionBursts.parameterize(system);
		startTotalTimeToLive.parameterize(system);
		startSpeed.parameterize(system);
		startRotation.parameterize(system);
		startRotationSpeed.parameterize(system);
		startSize.parameterize(system);
		startScaleX.parameterize(system);
		startScaleY.parameterize(system);
		startColor.parameterize(system);
		startMass.parameterize(system);

		foreach (var affector in affectors) {
			affector.updateParams (system);
		}

		foreach (var handle in handles) {
			handle.updateParams (system);
		}

		if (render) {
			render.updateParams (system);
		}
	}
	#endregion

	#region - Serialization
	public virtual JsonData save (JsonData systemData)
	{
		JsonData ret = new JsonData ();

		ret ["type"] = GetType ().Name;
		ret ["name"] = emitterName;
		
		ret ["position"] = PUEXUtils.serializeVector3(transform.localPosition);
		ret ["rotation"] = PUEXUtils.serializeQuaternion(transform.localRotation);
		ret ["scale"] = PUEXUtils.serializeVector3(transform.localScale);
		ret ["position_param_name"] = positionParamName;
		ret ["rotation_param_name"] = rotationParamName;
		ret ["scale_param_name"] = scaleParamName;

		ret ["duration"] = duration;
		ret ["looping"] = looping;
		ret ["loop_count"] = loopCount;
		ret ["prewarm"] = prewarm;
		ret ["start_delay"] = startDelay;
		ret ["keep_local"] = keepLocal;
		ret ["orientation_follow_direction"] = orientationFollowDirection;
		ret ["max_particles"] = maxParticles;

		ret ["burst_map"] = burstMap;
		ret ["emission_rate"] = PUEXUtils.serializeDynamicAttribute (emissionRate);
		ret ["emission_bursts"] = PUEXUtils.serializeDynamicTrigger (emissionBursts);
		ret ["emission_time_step"] = emissionTimeStep;

		ret ["start_total_time_to_live"] = PUEXUtils.serializeDynamicAttribute (startTotalTimeToLive);
		ret ["start_speed"] = PUEXUtils.serializeDynamicAttribute (startSpeed);
		ret ["start_rotation"] = PUEXUtils.serializeDynamicAttribute (startRotation);
		ret ["start_rotation_speed"] = PUEXUtils.serializeDynamicAttribute (startRotationSpeed);
		ret ["start_size"] = PUEXUtils.serializeDynamicAttribute (startSize);
		ret ["start_scale_x"] = PUEXUtils.serializeDynamicAttribute (startScaleX);
		ret ["start_scale_y"] = PUEXUtils.serializeDynamicAttribute (startScaleY);
		ret ["start_color"] = PUEXUtils.serializeDynamicColor (startColor);
		ret ["uv_tiles"] = PUEXUtils.serializeVector2 (uvTiles);
		ret ["start_random_frame"] = startRandomFrame;
		ret ["start_mass"] = PUEXUtils.serializeDynamicAttribute (startMass);
		ret ["rotation_axis"] = PUEXUtils.serializeVector3 (rotationAxis);

		if (gameObject.activeSelf && enabled)
			ret ["auto_start"] = true;
		else
			ret ["auto_start"] = false;

		PUEXAffector[] affectors_ = GetComponents<PUEXAffector> ();
		JsonData affectorsArray = new JsonData ();
		affectorsArray.SetJsonType (JsonType.Array);
		for (int i = 0; i < affectors_.Length; i++) {
			PUEXAffector affector = affectors_[i];
			JsonData affectorData = affector.save (ret, systemData);
			affectorsArray.Add (affectorData);
		}
		ret ["affectors"] = affectorsArray;

		PUEXHandle[] handles_ = GetComponents<PUEXHandle> ();
		JsonData handlesArray = new JsonData ();
		handlesArray.SetJsonType (JsonType.Array);
		for (int i = 0; i < handles_.Length; i++) {
			PUEXHandle handle = handles_[i];
			JsonData handleData = handle.save (ret, systemData);
			handlesArray.Add (handleData);
		}
		ret ["handles"] = handlesArray;

		PUEXRender render_ = GetComponent<PUEXRender> ();
		if (render_) {
			JsonData renderData = render_.save (ret, systemData);
			ret ["render"] = renderData;
		}
		
		return ret;
	}
	#endregion

}
